import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;

public class EntryPoint {

	public synchronized static void log(String s) {
		System.out.println(s);
	}

	public static void main(String[] args) throws InterruptedException {
		Object consistencyMutex = new Object();
		SharedMemory mem = new SharedMemory();
		CrawlerCommunicator crawlerSide = new CrawlerCommunicator(mem,
				consistencyMutex);
		GUICommunicator guiSide = new GUICommunicator(mem, consistencyMutex);
		crawlerSide.start();
		log("CrawlerSide started");
		guiSide.start();
		log("GUISide started");
		crawlerSide.join();
		guiSide.join();
	}
}

class SharedMemory {
	private LinkedList<Byte> GUItoCrawlerPipe = new LinkedList<Byte>();
	private LinkedList<Byte> CrawlertoGUIPipe = new LinkedList<Byte>();

	/**
	 * return number of bytes, the bytes contains one entry "url'\n'response_time'\n'"
	 * @return
	 */
	public synchronized int nextConsistentByteNum() {
		LinkedList<Byte> buffer = new LinkedList<Byte>();
		int byteNumber = 0;
		int Ncount = 0;
		while (!CrawlertoGUIPipe.isEmpty()) {
			Byte b = CrawlertoGUIPipe.pollFirst();
			buffer.addLast(b);
			if (b == '\n') {
				Ncount++;
				if (Ncount == 2) {
					byteNumber = buffer.size();
					break;
				}
			}
		}
		
		while (!buffer.isEmpty()) {
			CrawlertoGUIPipe.addFirst(buffer.pollLast());
		}
		return byteNumber;
	}
	
	public synchronized boolean isGUItoCrawlerEmpty() {
		return GUItoCrawlerPipe.isEmpty();
	}

	public synchronized byte nextGUItoCrawlerByte() {
		byte b = GUItoCrawlerPipe.pollFirst();
		EntryPoint.log("GUI --> Crawler remove:" + b);
		if (b == '\n') {
			b = '\0';
		}
		return b;
	}

	public synchronized void putGUItoCrawlerByte(Byte b) {
		EntryPoint.log("GUI --> Crawler add:" + b);
		GUItoCrawlerPipe.addLast(b);
	}

	public synchronized byte nextCrawlertoGUIByte() {
		byte b = this.CrawlertoGUIPipe.pollFirst();
		EntryPoint.log("Crawler --> GUI remove:" + b);
		return b;
	}

	public synchronized void putCrawlertoGUI(byte b) {
		EntryPoint.log("Crawler --> GUI add:" + b);
		if (b == '\0') {
			b = '\n';
		}
		this.CrawlertoGUIPipe.addLast(b);
	}

	public boolean isCrawlerToGUIEmpty() {
		return this.CrawlertoGUIPipe.isEmpty();
	}
}

class CrawlerCommunicator extends Thread {
	private final int PORTNUMBER = 31038;
	private SharedMemory shareMemo;
	private Object mutex;

	public CrawlerCommunicator(SharedMemory mem, Object consistencyMutex) {
		shareMemo = mem;
		mutex = consistencyMutex;
	}

	class ToCrawler extends Thread {
		OutputStream out;

		public ToCrawler(OutputStream o) {
			out = o;
		}

		@Override
		public void run() {
			while (true) {
				if (!shareMemo.isGUItoCrawlerEmpty()) {
					try {
						out.write(shareMemo.nextGUItoCrawlerByte());
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	class FromCrawler extends Thread {
		InputStream in;

		public FromCrawler(InputStream i) {
			in = i;
		}

		@Override
		public void run() {
			byte[] b = new byte[1];
			try {
				while (in.read(b, 0, 1) == 1) {
					shareMemo.putCrawlertoGUI(b[0]);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	public void run() {
		// crawler will always remain the connection
		try (Socket echoSocket = new Socket("localhost", PORTNUMBER);
				OutputStream out = echoSocket.getOutputStream();
				InputStream in = echoSocket.getInputStream();) {
			ToCrawler to = new ToCrawler(out);
			FromCrawler from = new FromCrawler(in);

			EntryPoint.log("Crawler side connected");
			to.start();
			from.start();

			to.join();
			from.join();

		} catch (UnknownHostException e) {
			System.out.println("Don't know about host localhost");
			System.exit(1);
		} catch (IOException e) {
			System.out
					.println("Couldn't get I/O for the connection to localhost");
			System.exit(1);
		} catch (InterruptedException e) {
			System.out.println("Exception on join");
			e.printStackTrace();
		}
	}
}

class GUICommunicator extends Thread {
	private final int PORTNUMBER = 31039;
	private SharedMemory shareMemo;
	private Object mutex;

	public GUICommunicator(SharedMemory mem, Object consistencyMutex) {
		shareMemo = mem;
		mutex = consistencyMutex;
	}

	@Override
	public void run() {
		while (true) {
			try (ServerSocket serverSocket = new ServerSocket(PORTNUMBER);
					Socket clientSocket = serverSocket.accept();
					OutputStream out = clientSocket.getOutputStream();
					InputStream in = clientSocket.getInputStream();) {
				synchronized (mutex) {
					EntryPoint.log("GUI Side connected");
					int c = fromGUI(in);
					if (c == -1) {
						toGUI(out);
					}
					clientSocket.close();
				}
			} catch (IOException e) {
				System.out
						.println("Exception caught when trying to listen on port "
								+ PORTNUMBER + " or listening for a connection");
				System.out.println(e.getMessage());
			}
		}
	}

	private void toGUI(OutputStream out) throws IOException {
		int numURL = 0;
		while (!shareMemo.isCrawlerToGUIEmpty()) {
			int numByteToSend = shareMemo.nextConsistentByteNum();
			for (int i = 0; i < numByteToSend; i++) {
				out.write(shareMemo.nextCrawlertoGUIByte());
			}
			numURL++;
			EntryPoint.log("["+numURL+"] URL has been sent");
			/**
			 * if the above log continues to appear with the same numURL
			 * it reflects that the CrawlertoGUI pipe contain an inconsistent
			 * entry with no more than 2 '/n' exist, which is not supposed to happen
			 */
		}
	}

	private int fromGUI(InputStream in) throws IOException {
		int re = -1;
		byte[] b = new byte[1];
		int c = in.read(b, 0, 1);
		if (b[0] == 10) { // 255 empty put
			return re;
		}
		EntryPoint.log("read returns: " + b[0]);
		if(b[0]==0)
		{
		while (c == 1) {
			shareMemo.putGUItoCrawlerByte(b[0]);
			c = in.read(b, 0, 1);
		}
		}
		else if (b[0]==1||b[0]==2)
		{
			shareMemo.putGUItoCrawlerByte(b[0]);
		}
		
		return re;
	}
}